namespace Daemon.Repository.EF
{
    using Daemon.Infrustructure.EF;
    using Daemon.Repository.Contract;
    using Daemon.Model;
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    using Daemon.Repository.EF.Enums;
    using Daemon.Common.Helpers;
    using Microsoft.AspNetCore.Http;
    using System.Collections.Generic;
    using Daemon.Data.Substructure.Enums;
    using Daemon.Model.Entities;
    using System.Reflection;
    using System.Globalization;
    public class TrackChangesBaseRepository<T, TK> : Repository<T, int>, ITrackChangesBaseRepository<T, int>
      where T : class, new()
    {
        protected override Expression<Func<T, int>> PrimaryKeyExpression => null;

        private readonly ILogRepository _logRepository;

        private readonly IHttpContextAccessor _httpContext;

        public TrackChangesBaseRepository(ApiDBContent context, ILogRepository logRepository) : base(context)
        {
            _logRepository = logRepository;
            _httpContext = Daemon.Middleware.ServiceLocator.Resolve<IHttpContextAccessor>();
        }

        protected override void UpdateEntitiesAfterCreate(IEnumerable<T> entities, ApiDBContent context)
        {
            LogAddedEntities(entities);
            base.UpdateEntitiesAfterCreate(entities, context);
        }

        protected override void UpdateEntitiesBeforeUpdate(IEnumerable<T> entities, ApiDBContent context)
        {
            LogEditedEntities(entities);
            base.UpdateEntitiesBeforeUpdate(entities, context);
        }

        protected override int BulkDeleteRange(IQueryable<T> entities, ApiDBContent context)
        {
            LogDeletedEntities(entities);
            return base.BulkDeleteRange(entities, context);
        }

        private void LogAddedEntities(IEnumerable<T> entities)
        {
            SaveLog(entities, EntityChangeType.I);
        }

        private void LogEditedEntities(IEnumerable<T> entities)
        {
            SaveLog(entities, EntityChangeType.U);
        }

        private void LogDeletedEntities(IQueryable<T> entities)
        {
            SaveLog(entities, EntityChangeType.D);
        }

        private void SaveLog(IEnumerable<T> entities, EntityChangeType changeType)
        {
            if (entities.Count() > 0)
            {
                var nowTime = DateTime.UtcNow;
                var logRules = _logRepository.GetLogRules(typeof(T), changeType);

                List<T> originalEntities = null;
                if (changeType == EntityChangeType.U && logRules.Count() > 0)
                {
                    var ids = entities.Select(PrimaryKeyExpression.Compile()).ToList();
                    originalEntities = FindByIds(ids).ToList();
                }

                foreach (var logRule in logRules)
                {
                    SaveLogForEntities(logRule, entities, originalEntities, nowTime, changeType);
                }
            }
        }

        private void SaveLogForEntities(LogRule logRule, IEnumerable<T> newEntities, IEnumerable<T> originalEntities, DateTime occuredTime, EntityChangeType changeType)
        {
            var entityType = typeof(T);
            GetLogRuleRelatedEntities(logRule, entityType, out List<LogRuleMonitoringField> logRuleMonitoringFields, out List<LogRuleReport> logRuleReports, out List<LogRuleCalculatedField> logRuleCalculatedFields, out List<LogEntityHardcodedValue> logEntityHardcodedValues, out List<ConstraintTableInfo> constraintTableInfos);
            var primaryKeyProperty = entityType.GetProperty(GetPrimaryKeys());

            foreach (var entity in newEntities)
            {
                T originalEntity = default(T);
                if (changeType == EntityChangeType.U)
                {
                    var id = (int)primaryKeyProperty.GetValue(entity);
                    originalEntity = originalEntities.FirstOrDefault(GetQueryExpression(PrimaryKeyExpression, id).Compile());
                }

                var logEntityEvent = SaveLogEntityEvent(logRule, entity, occuredTime);
                var trackedFields = GetTrackedFields(logRule, entity, originalEntity, logRuleMonitoringFields, changeType);
                var reportingEntities = GetReportingEntities(entity, logRuleReports, trackedFields);
                var calculatedFields = CalculateFieldValues(entity, logRuleCalculatedFields, trackedFields);
                SaveLogEntityEventTrackedChanges(logEntityEvent, trackedFields);
                SaveLogEntry(logRule, entity, trackedFields, logEntityEvent, reportingEntities, calculatedFields, logEntityHardcodedValues, constraintTableInfos, occuredTime);
            }
        }

        private List<ReportingEntityDto> GetReportingEntities(T entity, List<LogRuleReport> logRuleReports, List<TrackedField> trackedFields)
        {
            var reportingEntities = new List<ReportingEntityDto>();
            foreach (var logRuleReport in logRuleReports)
            {
                var reportingEntitySql = _logRepository.ReplaceTerms(logRuleReport.ReportingEntitySQL, entity, trackedFields, null, null, null);
                var reportingEntity = _logRepository.ExecuteReportingSql(reportingEntitySql);
                reportingEntities.Add(reportingEntity);
            }

            return reportingEntities;
        }

        private List<LogRuleCalculatedField> CalculateFieldValues(T entity, List<LogRuleCalculatedField> logRuleCalculatedFields, List<TrackedField> trackedFields)
        {
            foreach (var logRuleCalculatedField in logRuleCalculatedFields)
            {
                var calculationSql = _logRepository.ReplaceTerms(logRuleCalculatedField.CalculationSql, entity, trackedFields, null, null, null);
                logRuleCalculatedField.Value = _logRepository.ExecuteCalculationSql(calculationSql);
            }

            return logRuleCalculatedFields;
        }

        private void GetLogRuleRelatedEntities(LogRule logRule, Type entityType, out List<LogRuleMonitoringField> logRuleMonitoringFields, out List<LogRuleReport> logRuleReports, out List<LogRuleCalculatedField> logRuleCalculatedFields, out List<LogEntityHardcodedValue> logEntityHardcodedValues, out List<ConstraintTableInfo> constraintTableInfos)
        {
            logRuleMonitoringFields = _logRepository.GetLogRuleMonitoringFields(logRule.Id);
            logRuleCalculatedFields = _logRepository.GetLogRuleCalculatedFields(logRule.Id);
            logRuleReports = _logRepository.GetLogRuleReports(logRule.Id);
            logEntityHardcodedValues = _logRepository.GetLogEntityHardcodedValues(entityType.Name);
            constraintTableInfos = _logRepository.GetReferencedTableInfos(entityType.Name);
        }

        private LogEntityEvent SaveLogEntityEvent(LogRule logRule, T entity, DateTime occuredTime)
        {
            var logEntityEvent = new LogEntityEvent
            {
                LogRuleId = logRule.Id,
                EntityPKValue = PrimaryKeyExpression.Compile().Invoke(entity).ToString(),
                UserId = Convert.ToInt32(_httpContext.HttpContext.User.Claims.FirstOrDefault(r => r.Type == "UserId")?.Value),
                DateOccured = occuredTime,
                TransactionGuid = null,
            };
            _logRepository.SaveLogEntityEvent(logEntityEvent);
            return logEntityEvent;
        }

        private List<TrackedField> GetTrackedFields(LogRule logRule, T entity, T originalEntity, IEnumerable<LogRuleMonitoringField> logRuleMonitoringFields, EntityChangeType changeType)
        {
            var trackedFields = new List<TrackedField>();
            var entityType = typeof(T);
            foreach (var monitoringField in logRuleMonitoringFields)
            {
                var propertyInfo = entityType.GetProperty(monitoringField.FieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if (propertyInfo != null)
                {
                    var oldValue = changeType == EntityChangeType.U ? propertyInfo.GetValue(originalEntity) :
                                   changeType == EntityChangeType.D ? propertyInfo.GetValue(entity) : null;

                    var newValue = changeType == EntityChangeType.D ? null : propertyInfo.GetValue(entity);
                    var changed = changeType == EntityChangeType.U ? !IsSameValue(oldValue, newValue) : false;

                    trackedFields.Add(new TrackedField
                    {
                        LogRuleId = logRule.Id,
                        ChangeType = changeType,
                        Id = monitoringField.Id,
                        FieldName = monitoringField.FieldName,
                        DisplayName = monitoringField.DisplayName,
                        NewVal = newValue?.ToString(),
                        OldVal = oldValue?.ToString(),
                        Changed = changed,
                        ValueType = propertyInfo.PropertyType,
                    });
                }
            }

            return trackedFields;
        }

        private void SaveLogEntityEventTrackedChanges(LogEntityEvent logEntityEvent, IEnumerable<TrackedField> trackedFields)
        {
            var logEntityEventTrackedChanges = new List<LogEntityEventTrackedChange>();
            foreach (var trackedField in trackedFields.Where(t => t.Changed))
            {
                logEntityEventTrackedChanges.Add(new LogEntityEventTrackedChange
                {
                    FieldName = trackedField.FieldName,
                    DisplayName = trackedField.DisplayName,
                    LogEntityEventId = logEntityEvent.Id,
                    LogRuleMonitoringFieldId = trackedField.Id,
                    NewVal = trackedField.NewVal,
                    OldVal = trackedField.OldVal,
                });
            }

            _logRepository.SaveLogEntityEventTrackedChanges(logEntityEventTrackedChanges);
        }

        private void SaveLogEntry(LogRule logRule, T entity, IEnumerable<TrackedField> trackedFields, LogEntityEvent logEntityEvent, List<ReportingEntityDto> reportingEntities, List<LogRuleCalculatedField> logRuleCalculatedFields, List<LogEntityHardcodedValue> logEntityHardcodedValues, List<ConstraintTableInfo> constraintTableInfos, DateTime occuredTime)
        {
            var entityType = typeof(T);
            foreach (var trackedField in trackedFields)
            {
                ReplaceEntityFieldWithFKValue(constraintTableInfos, trackedField, entityType);
                ReplaceEntityFieldWithHardCodedValues(trackedField, logEntityHardcodedValues);
            }

            var keyWords = GetKeyWords(trackedFields, occuredTime);

            foreach (var reportingEntity in reportingEntities)
            {
                var logEntry = new LogEntry
                {
                    LogEntityEventId = logEntityEvent.Id,
                    LogRuleId = logRule.Id,
                    LogTime = occuredTime,
                    UserId = 1,
                    UserName = "dgq",
                    ReportingEntity = reportingEntity.ReportingEntity,
                    ReportingEntityPKVAL = reportingEntity.ReportingPK.ToString(),
                    LogEntry1 = _logRepository.FormatLogEntry(entity, logRule.LogMacro, trackedFields, logRuleCalculatedFields, constraintTableInfos, keyWords),
                };

                _logRepository.SaveLogEntry(logEntry);
            }
        }

        private void ReplaceEntityFieldWithFKValue(List<ConstraintTableInfo> constraintTableInfos, TrackedField trackedField, Type entityType)
        {
            if (!string.IsNullOrEmpty(trackedField.OldVal))
            {
                trackedField.OldVal = GetEntityFKValueByReferencedKey(constraintTableInfos, trackedField.FieldName, entityType.Name, trackedField.OldVal);
            }

            if (!string.IsNullOrEmpty(trackedField.NewVal))
            {
                trackedField.NewVal = GetEntityFKValueByReferencedKey(constraintTableInfos, trackedField.FieldName, entityType.Name, trackedField.NewVal);
            }
        }

        private void ReplaceEntityFieldWithHardCodedValues(TrackedField trackedField, List<LogEntityHardcodedValue> logEntityHardcodedValues)
        {
            if (!string.IsNullOrEmpty(trackedField.NewVal))
            {
                var value = logEntityHardcodedValues.FirstOrDefault(l => l.Field.IgnoreCaseEquals(trackedField.FieldName) && l.KeyValue == trackedField.NewVal);
                if (value != null)
                {
                    trackedField.NewVal = value.KeyReplacement;
                }
            }

            if (!string.IsNullOrEmpty(trackedField.OldVal))
            {
                var value = logEntityHardcodedValues.FirstOrDefault(l => l.Field.IgnoreCaseEquals(trackedField.FieldName) && l.KeyValue == trackedField.OldVal);
                if (value != null)
                {
                    trackedField.OldVal = value.KeyReplacement;
                }
            }
        }

        private string GetEntityFKValueByReferencedKey(List<ConstraintTableInfo> constraintTableInfos, string fieldName, string entityTypeName, string fieldValue)
        {
            var constraintTableInfo = constraintTableInfos.FirstOrDefault(t => t.ReferencingTableName.IgnoreCaseEquals(entityTypeName) && t.ReferencingColumnName.IgnoreCaseEquals(fieldName));
            if (constraintTableInfo != null)
            {
                // Look up Entity in EntityMeta
                var relatedEntityMeta = _logRepository.GetEntityMetaByEntityName(constraintTableInfo.ReferencedTableName);
                if (relatedEntityMeta != null)
                {
                    return _logRepository.GetValueByEntityMetaReferencedInfo(relatedEntityMeta.Entity, relatedEntityMeta.DescriptiveNameFields, relatedEntityMeta.PKField, fieldValue);
                }
            }

            return fieldValue;
        }

        private Dictionary<string, string> GetKeyWords(IEnumerable<TrackedField> trackedFields, DateTime occuredTime)
        {
            var fieldsChangedString = GenerateFieldsChangedMacro(trackedFields);
            var userFullName = _httpContext.HttpContext.User.Claims.FirstOrDefault(r => r.Type == "UserName")?.Value;
            var occuredTimeString = DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss tt");
            var keyWords = new Dictionary<string, string>()
                {
                    { "CHANGEINFO", $" by {userFullName} on {occuredTimeString}" },
                    { "FIELDSCHANGED",  fieldsChangedString },
                };

            return keyWords;
        }

        private string GenerateFieldsChangedMacro(IEnumerable<TrackedField> trackedFields)
        {
            var changedStrings = trackedFields.Where(t => t.Changed).Select(f =>
               {
                   return $"{f.DisplayName} 从 “{_logRepository.TextValue(f.OldVal)}” 修改成 “{_logRepository.TextValue(f.NewVal)}“";
               });

            return string.Join(", ", changedStrings);
        }

        private static bool IsSameValue(object a, object b)
        {
            if (a == null && b == null)
            {
                return true;
            }

            if (a != null && b != null)
            {
                return a.Equals(b);
            }

            return false;
        }
    }
}